{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a2daf662",
   "metadata": {},
   "source": [
    "\n",
    "[Deep Residual Learning for Image Recognition.pdf](./Deep%20Residual%20Learning%20for%20Image%20Recognition.pdf)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "79d48e8f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import torch\n",
    "import torchvision as tv\n",
    "from torch import nn\n",
    "from PIL import Image\n",
    "from torchvision import transforms\n",
    "from torch.nn import functional as F\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "3f17b6cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "device = torch.device('cuda' if torch.cuda.is_available else 'cpu')\n",
    "batch_size = 192\n",
    "num_epoch = 10\n",
    "num_class = 10 # cifar10\n",
    "lr = 1e-3\n",
    "loss_func = nn.CrossEntropyLoss()\n",
    "# torch.autograd.set_detect_anomaly(True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "71c4a88f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([3, 32, 32]) 6\n"
     ]
    }
   ],
   "source": [
    "import torchvision as tv\n",
    "from torchvision import transforms\n",
    "\n",
    "\n",
    "transform = transforms.Compose([\n",
    "    # transforms.Resize((224,224)),\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))\n",
    "])\n",
    "\n",
    "train_data = tv.datasets.cifar.CIFAR10(\"./cifar10_dataset\",train=True,  download=True, transform=transform)\n",
    "test_data = tv.datasets.cifar.CIFAR10(\"./cifar10_dataset\",train=False,  download=True, transform=transform)\n",
    "\n",
    "train_iter = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=True)\n",
    "test_iter = torch.utils.data.DataLoader(test_data, batch_size=batch_size, shuffle=False)\n",
    "\n",
    "for x, y in train_data:\n",
    "    print(x.shape, y)\n",
    "    break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ad621d60",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch import Tensor\n",
    "\n",
    "\n",
    "class ResBlock(nn.Module):\n",
    "    def __init__(self, in_channels: int, out_channels: int, stride: int = 1):\n",
    "        \"\"\"\n",
    "        stride: 第一层是否下采样\n",
    "        \"\"\"\n",
    "        super(ResBlock, self).__init__()\n",
    "        self.layer1 = nn.Sequential(\n",
    "            nn.Conv2d(in_channels, out_channels, 3, padding=1, stride=stride),\n",
    "            nn.BatchNorm2d(out_channels),\n",
    "        )\n",
    "        self.layer2 = nn.Sequential(\n",
    "            nn.Conv2d(out_channels, out_channels, 3, padding=1, stride=1),\n",
    "            nn.BatchNorm2d(out_channels),\n",
    "        )\n",
    "        self.shortcut = nn.Sequential()\n",
    "        if stride != 1 or in_channels != out_channels:\n",
    "            self.shortcut = nn.Sequential(\n",
    "                nn.Conv2d(in_channels, out_channels, 1, stride=stride),\n",
    "                nn.BatchNorm2d(out_channels),\n",
    "            )\n",
    "\n",
    "    def forward(self, x: Tensor):\n",
    "        result = self.layer2(self.layer1(x))\n",
    "        result += self.shortcut(x)\n",
    "        return F.relu(result)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "f3c83daa",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ResNet(nn.Module):\n",
    "    def __init__(self, num_class=10):\n",
    "        super(ResNet, self).__init__()\n",
    "        self.num_classes = num_class\n",
    "        self.layer0 = nn.Sequential(\n",
    "            nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),\n",
    "            nn.BatchNorm2d(64),\n",
    "            nn.ReLU(),\n",
    "        )\n",
    "        self.layer1 = nn.Sequential(\n",
    "            ResBlock(in_channels=64, out_channels=64, stride=1),\n",
    "            ResBlock(in_channels=64, out_channels=64, stride=1),\n",
    "        )\n",
    "        self.layer2 = nn.Sequential(\n",
    "            ResBlock(in_channels=64, out_channels=128, stride=2),\n",
    "            ResBlock(in_channels=128, out_channels=128, stride=1),\n",
    "        )\n",
    "        self.layer3 = nn.Sequential(\n",
    "            ResBlock(in_channels=128, out_channels=256, stride=2),\n",
    "            ResBlock(in_channels=256, out_channels=256, stride=1),\n",
    "        )\n",
    "        self.layer4 = nn.Sequential(\n",
    "            ResBlock(in_channels=256, out_channels=512, stride=2),\n",
    "            ResBlock(in_channels=512, out_channels=512, stride=1),\n",
    "        )\n",
    "        self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))\n",
    "        self.fc = nn.Linear(512, self.num_classes)\n",
    "\n",
    "    def forward(self, x: Tensor):\n",
    "        out = self.layer0(x)\n",
    "        out = self.layer1(out)\n",
    "        out = self.layer2(out)\n",
    "        out = self.layer3(out)\n",
    "        out = self.layer4(out)\n",
    "        out = self.avg_pool(out)\n",
    "        out = torch.flatten(out, 1)\n",
    "        out = self.fc(out)\n",
    "        return out"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "id": "94f2bc95",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "=================================================================\n",
      "Layer (type:depth-idx)                   Param #\n",
      "=================================================================\n",
      "├─Sequential: 1-1                        --\n",
      "|    └─Conv2d: 2-1                       1,792\n",
      "|    └─BatchNorm2d: 2-2                  128\n",
      "|    └─ReLU: 2-3                         --\n",
      "├─Sequential: 1-2                        --\n",
      "|    └─ResBlock: 2-4                     --\n",
      "|    |    └─Sequential: 3-1              37,056\n",
      "|    |    └─Sequential: 3-2              37,056\n",
      "|    |    └─Sequential: 3-3              --\n",
      "|    └─ResBlock: 2-5                     --\n",
      "|    |    └─Sequential: 3-4              37,056\n",
      "|    |    └─Sequential: 3-5              37,056\n",
      "|    |    └─Sequential: 3-6              --\n",
      "├─Sequential: 1-3                        --\n",
      "|    └─ResBlock: 2-6                     --\n",
      "|    |    └─Sequential: 3-7              74,112\n",
      "|    |    └─Sequential: 3-8              147,840\n",
      "|    |    └─Sequential: 3-9              8,576\n",
      "|    └─ResBlock: 2-7                     --\n",
      "|    |    └─Sequential: 3-10             147,840\n",
      "|    |    └─Sequential: 3-11             147,840\n",
      "|    |    └─Sequential: 3-12             --\n",
      "├─Sequential: 1-4                        --\n",
      "|    └─ResBlock: 2-8                     --\n",
      "|    |    └─Sequential: 3-13             295,680\n",
      "|    |    └─Sequential: 3-14             590,592\n",
      "|    |    └─Sequential: 3-15             33,536\n",
      "|    └─ResBlock: 2-9                     --\n",
      "|    |    └─Sequential: 3-16             590,592\n",
      "|    |    └─Sequential: 3-17             590,592\n",
      "|    |    └─Sequential: 3-18             --\n",
      "├─Sequential: 1-5                        --\n",
      "|    └─ResBlock: 2-10                    --\n",
      "|    |    └─Sequential: 3-19             1,181,184\n",
      "|    |    └─Sequential: 3-20             2,360,832\n",
      "|    |    └─Sequential: 3-21             132,608\n",
      "|    └─ResBlock: 2-11                    --\n",
      "|    |    └─Sequential: 3-22             2,360,832\n",
      "|    |    └─Sequential: 3-23             2,360,832\n",
      "|    |    └─Sequential: 3-24             --\n",
      "├─AdaptiveAvgPool2d: 1-6                 --\n",
      "├─Linear: 1-7                            5,130\n",
      "=================================================================\n",
      "Total params: 11,178,762\n",
      "Trainable params: 11,178,762\n",
      "Non-trainable params: 0\n",
      "=================================================================\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "=================================================================\n",
       "Layer (type:depth-idx)                   Param #\n",
       "=================================================================\n",
       "├─Sequential: 1-1                        --\n",
       "|    └─Conv2d: 2-1                       1,792\n",
       "|    └─BatchNorm2d: 2-2                  128\n",
       "|    └─ReLU: 2-3                         --\n",
       "├─Sequential: 1-2                        --\n",
       "|    └─ResBlock: 2-4                     --\n",
       "|    |    └─Sequential: 3-1              37,056\n",
       "|    |    └─Sequential: 3-2              37,056\n",
       "|    |    └─Sequential: 3-3              --\n",
       "|    └─ResBlock: 2-5                     --\n",
       "|    |    └─Sequential: 3-4              37,056\n",
       "|    |    └─Sequential: 3-5              37,056\n",
       "|    |    └─Sequential: 3-6              --\n",
       "├─Sequential: 1-3                        --\n",
       "|    └─ResBlock: 2-6                     --\n",
       "|    |    └─Sequential: 3-7              74,112\n",
       "|    |    └─Sequential: 3-8              147,840\n",
       "|    |    └─Sequential: 3-9              8,576\n",
       "|    └─ResBlock: 2-7                     --\n",
       "|    |    └─Sequential: 3-10             147,840\n",
       "|    |    └─Sequential: 3-11             147,840\n",
       "|    |    └─Sequential: 3-12             --\n",
       "├─Sequential: 1-4                        --\n",
       "|    └─ResBlock: 2-8                     --\n",
       "|    |    └─Sequential: 3-13             295,680\n",
       "|    |    └─Sequential: 3-14             590,592\n",
       "|    |    └─Sequential: 3-15             33,536\n",
       "|    └─ResBlock: 2-9                     --\n",
       "|    |    └─Sequential: 3-16             590,592\n",
       "|    |    └─Sequential: 3-17             590,592\n",
       "|    |    └─Sequential: 3-18             --\n",
       "├─Sequential: 1-5                        --\n",
       "|    └─ResBlock: 2-10                    --\n",
       "|    |    └─Sequential: 3-19             1,181,184\n",
       "|    |    └─Sequential: 3-20             2,360,832\n",
       "|    |    └─Sequential: 3-21             132,608\n",
       "|    └─ResBlock: 2-11                    --\n",
       "|    |    └─Sequential: 3-22             2,360,832\n",
       "|    |    └─Sequential: 3-23             2,360,832\n",
       "|    |    └─Sequential: 3-24             --\n",
       "├─AdaptiveAvgPool2d: 1-6                 --\n",
       "├─Linear: 1-7                            5,130\n",
       "=================================================================\n",
       "Total params: 11,178,762\n",
       "Trainable params: 11,178,762\n",
       "Non-trainable params: 0\n",
       "================================================================="
      ]
     },
     "execution_count": 50,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from torchsummary import summary\n",
    "\n",
    "summary(ResNet(10))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "8a02fcb0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ 1/10]\t train_loss=0.0079, test_loss=0.0067, train_acc=0.4459 test_acc=0.5312\n",
      "[ 2/10]\t train_loss=0.0052, test_loss=0.0053, train_acc=0.6409 test_acc=0.6484\n",
      "[ 3/10]\t train_loss=0.0041, test_loss=0.0040, train_acc=0.7203 test_acc=0.7378\n",
      "[ 4/10]\t train_loss=0.0034, test_loss=0.0036, train_acc=0.7727 test_acc=0.7666\n",
      "[ 5/10]\t train_loss=0.0028, test_loss=0.0041, train_acc=0.8102 test_acc=0.7374\n",
      "[ 6/10]\t train_loss=0.0024, test_loss=0.0034, train_acc=0.8416 test_acc=0.7872\n",
      "[ 7/10]\t train_loss=0.0020, test_loss=0.0031, train_acc=0.8635 test_acc=0.8035\n",
      "[ 8/10]\t train_loss=0.0017, test_loss=0.0029, train_acc=0.8848 test_acc=0.8136\n",
      "[ 9/10]\t train_loss=0.0014, test_loss=0.0032, train_acc=0.9014 test_acc=0.8102\n",
      "[10/10]\t train_loss=0.0011, test_loss=0.0036, train_acc=0.9248 test_acc=0.8052\n"
     ]
    }
   ],
   "source": [
    "net = ResNet(num_class).to(device)\n",
    "optimizer = torch.optim.Adam(net.parameters(), lr=lr)\n",
    "\n",
    "train_loss, test_loss, train_acc, test_acc = [], [], [], []\n",
    "\n",
    "for epoch in range(num_epoch):\n",
    "    net.train()\n",
    "    train_loss_epoch, train_acc_epoch, img_cnt = 0, 0, 0\n",
    "    for x, y in train_iter:\n",
    "        x, y = x.to(device), y.to(device)\n",
    "        pred: Tensor = net(x)\n",
    "        los = loss_func(pred, y)\n",
    "        los.backward()\n",
    "        optimizer.step()\n",
    "        optimizer.zero_grad()\n",
    "        train_loss_epoch += los.item()\n",
    "        img_cnt += y.shape[0]\n",
    "        train_acc_epoch += (pred.argmax(dim=1) == y).sum().item()\n",
    "    train_loss_epoch /= img_cnt\n",
    "    train_acc_epoch /= img_cnt\n",
    "    train_loss.append(train_loss_epoch)\n",
    "    train_acc.append(train_acc_epoch)\n",
    "    # ------\n",
    "    net.eval()\n",
    "    test_loss_epoch, img_cnt, test_acc_epoch = 0, 0, 0\n",
    "    with torch.no_grad():\n",
    "        for x, y in test_iter:\n",
    "            x, y = x.to(device), y.to(device)\n",
    "            pred: Tensor = net(x)\n",
    "            los = loss_func(pred, y)\n",
    "            test_loss_epoch += los.item()\n",
    "            test_acc_epoch += (pred.argmax(dim=1) == y).sum().item()\n",
    "            img_cnt += y.shape[0]\n",
    "\n",
    "    test_loss_epoch /= img_cnt\n",
    "    test_acc_epoch /= img_cnt\n",
    "\n",
    "    test_loss.append(test_loss_epoch)\n",
    "    test_acc.append(test_acc_epoch)\n",
    "\n",
    "    print(\n",
    "        f\"[{epoch+1:2d}/{num_epoch}]\\t train_loss={train_loss_epoch:.4f}, test_loss={test_loss_epoch:.4f}, train_acc={train_acc_epoch:.4f} test_acc={test_acc_epoch:.4f}\"\n",
    "    )\n",
    "    torch.save(net.state_dict(), f\"resnet_cifar10_{epoch+1:03d}.pth\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "17bb8953",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-0.9843137..0.94509804].\n",
      "Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-0.99215686..1.0].\n",
      "Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers). Got range [-0.9607843..0.8980392].\n"
     ]
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgMAAAC8CAYAAADl2K3eAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAMohJREFUeJztnX10VNW5/58chslkGIZhmAxhDEkMMYaImCKCIgVFKoqQW3sRX6oVr9VYr3rtr95W7LpX2tq7rL1Xrb1Lxeu9+FYroNaqpb5UQXyh+IoKGEKEIYYQYhiGYZhMToY5vz+6yOrez6M5DIEJnO9nLddyPzxnn33OnLOzM/ub71NgWZZFAAAAAHAsRr4HAAAAAID8gsUAAAAA4HCwGAAAAAAcDhYDAAAAgMPBYgAAAABwOFgMAAAAAA4HiwEAAADA4WAxAAAAADgcLAYAAAAAh4PFQJ6JRqNUUFBAjzzySL6HAsBBU1BQQIsWLcr3MAA4KDDvco6qxUB3dzf95Cc/oUgkQkVFRTR58mR69dVX8z2sAcVTTz1FEyZMII/HQ8XFxXT11VdTZ2dnvoflaDZs2EAXXXQRVVZWktfrpVAoRNOmTaMXXngh30MbMKxevZrq6+tp9OjR5PF4qKSkhM477zx6++238z00QEQffvgh1dfXUzAYJK/XS+PGjaP77rsv38MaUCxdupTOOOMMGjJkCAUCAZoyZQq9/vrr+R6WbVz5HsDBsGDBAnr66afp5ptvphNOOIEeeeQRmj17Nq1cuZKmTp2a7+HlnQceeICuv/56Ouecc+juu++m1tZW+s1vfkPvv/8+rV27ljweT76H6Ei2bdtGe/fupSuvvJIikQilUil65plnqL6+nhYvXkzXXnttvoeYd5qamsgwDLruuuuopKSEdu/eTU888QRNmzaN/vSnP9F5552X7yE6lldeeYXmzp1L3/jGN+jf/u3fyOfz0eeff06tra35HtqAYdGiRfTzn/+c5s2bRwsWLKCenh5av349bd++Pd9Ds491lLB27VqLiKxf//rXvbGuri5rzJgx1hlnnHHEx5PNZq1UKnXI/WzdutUiImvJkiWH1E93d7cVCASsadOmWdlstjf+wgsvWERk3XfffYc4UtCfZDIZ65RTTrFOPPHEvJx/3759/dIPEVm33357v/Sls2/fPmvkyJHWrFmzDkv/oG/27NljjRw50rrwwgut/fv353s4A27etSzLWrNmjVVQUGDdfffdh9xXPjlqtgmefvppGjRokPJblMfjoauvvprWrFlDX3zxxUH3+cgjj1BBQQGtXr2aGhoaaMSIEeT3++l73/se7d69W8mtqKigOXPm0Msvv0wTJ06koqIiWrx4MRERxeNxuvnmm2n06NFUWFhIVVVV9Ktf/Yqy2azSRzwepwULFtCwYcMoEAjQlVdeSfF4/OBvhsD69espHo/TxRdfTAUFBb3xOXPmkM/no6eeeqpfzgP6h0GDBtHo0aNz/vxXrVpFBQUFtHTpUrrtttuopKSEhgwZQvX19exdOOuss2jcuHH0wQcf0LRp08jr9dJtt91GRH/berv99tupqqqKCgsLafTo0fTjH/+Yuru7lT66u7vphz/8IRUXF9PQoUOpvr7+sP9m6PV6qbi4uN/eEXDwPPnkk7Rz50765S9/SYZh0L59+9i8drAcS/MuEdG9995LJSUl9C//8i9kWRYlk8l+6/tIctRsE3z00UdUXV1Nfr9fiU+aNImIiNatW0ejR4/Oqe8bbriBAoEALVq0iDZt2kQPPPAAbdu2rXfCPcCmTZvo0ksvpYaGBrrmmmvoxBNPpFQqRdOnT6ft27dTQ0MDlZWV0TvvvEMLFy6kHTt20L333ktERJZl0T/8wz/QW2+9Rddddx2NHTuW/vCHP9CVV17JxtPd3U179+61NfZQKNR7DBFRUVERyykqKqKPPvqIstksGcZRs/475ti3bx91dXXRnj176Pnnn6c///nPdPHFFx9Sn7/85S+poKCAfvKTn1BHRwfde++9NHPmTFq3bp3yLOzatYvOP/98uuSSS+jyyy+nkSNHUjabpfr6enrrrbfo2muvpbFjx9Knn35K99xzDzU1NdFzzz3Xe/z3v/99euKJJ+iyyy7r3Qu94IIL2Hh6enpoz549tsYeDAbZ85hIJMg0Ters7KTHHnuM1q9f37twAUeev/zlL+T3+2n79u307W9/m5qammjIkCF0xRVX0D333HNIW4/HwrxLRPTaa6/RlClT6L777qM77riDdu3aRSUlJfTTn/6UbrjhhpzvzxEnz99M2Oakk06yZsyYweIbNmywiMh68MEHD7rPJUuWWERknXrqqZZpmr3xu+66yyIi649//GNvrLy83CIi66WXXlL6+MUvfmENGTLEampqUuK33nqrNWjQIKulpcWyLMt67rnnLCKy7rrrrt6cTCZjffOb32RfVx0Yl53/DvDll19aBQUF1tVXX62Mo7GxsTe3s7PzoO8R6D8aGhp6PwvDMKx58+ZZsVgsp75WrlxpEZF13HHHWYlEoje+bNkyi4is3/zmN72x6dOni+/I448/bhmGYb355ptK/MEHH7SIyHr77bcty7KsdevWWURkXX/99UreZZddxrYJDozLzn9bt25l1zVr1qzef3e73VZDQ4PV1dWV0z0Ch8748eMtr9dreb1e68Ybb7SeeeYZ68Ybb7SIyLrkkkty6vNYmndjsZhFRNaIESMsn89n/frXv7aWLl1qnXfeeTn/XMoXR803A11dXVRYWMjiB1amXV1dOfd97bXX0uDBg3vbP/jBD+i2226jFStWUH19fW/8+OOPp1mzZinHLl++nL75zW/S8OHDFdX+zJkz6c4776TVq1fTd7/7XVqxYgW5XC76wQ9+0JszaNAguvHGG+nNN99U+pw1a9ZB/5VEKBSi+fPn06OPPkpjx46lCy+8kLZv30433ngjDR48mHp6eg7pHoFD5+abb6Z58+ZRW1sbLVu2jPbv30+maR5Sn9/73vdo6NChve158+bRqFGjaMWKFXTTTTf1xgsLC+mqq65Sjl2+fDmNHTuWampqlGd3xowZRES0cuVKmjJlCq1YsYKISOnvwPU8+eSTSuyUU06x/eyWlJSw2J133kk/+tGP6IsvvqBHH32UTNOkTCZjqz/Q/ySTSUqlUnTdddf1/vXAd77zHTJNkxYvXkw///nP6YQTTsip72Nh3j2wJbBr1y566qmner/pmzdvHp188sl0xx13UENDw0H1mS+OmsVAUVER28ckIkqn073/niv6w+zz+WjUqFEUjUaV+PHHH8+O3bx5M33yySdUXFws9t3R0UFEf1OUjxo1inw+n/LvJ554Ijtm1KhRNGrUqIO5BCIiWrx4MXV1ddEtt9xCt9xyCxERXX755TRmzBh69tln2bnBkaWmpoZqamqI6G8/xM8991yaO3curV27Vvla9GDQn92CggKqqqpiz+5xxx1HbrdbiW3evJk+++wzW8+uYRg0ZswY5d+lZ3f48OE0c+bMg72MXurq6nr///LLL6cJEyb0/hUROPIcmFcvvfRSJX7ZZZfR4sWLac2aNTkvBo6FeffA/Rk8eDDNmzevN24YBl188cV0++23U0tLC5WVlR1Uv/ngqFkMjBo1SvwzjR07dhARUSQSOexjkBYc2WyWvvWtb9GPf/xj8Zjq6uqDPs+BfWU7/P1vV8OGDaM//vGP1NLSQtFolMrLy6m8vJymTJlCxcXFFAgEDnos4PAxb948amhooKamJnFy6k++6tk9+eST6e677xaPyUWDY5omxWIxW7nFxcU0aNCgr/x3t9tN9fX1dOedd1JXV9chLfhBbkQiEdqwYQONHDlSiYfDYSIiJvg7HAzkeTcYDJLH46FAIMCe5b+/R1gM9CN1dXW0cuVKSiQSiohw7dq1vf+eK5s3b6azzz67t51MJmnHjh00e/bsPo8dM2YMJZPJPn8bKi8vp9dee42SyaSySt20aRPLXbp0KftK96uwLIvFysrKeh++eDxOH3zwAf3jP/6jrf7AkePAto3dCUhi8+bNStuyLGpubqbx48f3eeyYMWPo448/pnPOOedrv5koLy+nbDZLn3/+ubJokZ7dd955R3mXvo6tW7dSRUXF1+Z0dXWRZVm0d+9eLAbywKmnnkqvvvoqbd++Xfns29raiIi+8jdzOxwL865hGFRXV0fvvfcemaapfPvWH/foSHLUSMvnzZtH+/fvp4ceeqg31t3dTUuWLKHJkyfn/JcEREQPPfQQ9fT09LYfeOABymQydP755/d57Pz582nNmjX08ssvs3+Lx+O9+52zZ8+mTCZDDzzwQO+/79+/n37729+y4w7sXdn5ry8WLlxImUyGfvjDH/aZCw4PB76y/Ht6enroscceo6KiIqqtrc2578cee0xRQD/99NO0Y8cO28/u9u3b6X/+53/Yv3V1ddG+ffuIiHr70h3nDii2/54DmgE7//39t1rSPYrH4/TMM8/Q6NGje3/LAkeW+fPnExHR//7v/yrxhx9+mFwuF5111lk5932szLsXX3wx7d+/nx599NHeWDqdpt/97ndUW1t7RL617g+Omm8GJk+eTBdddBEtXLiQOjo6qKqqih599FGKRqPsQV20aBH97Gc/o5UrV9p6WE3TpHPOOYfmz59PmzZtovvvv5+mTp2qiFi+in/913+l559/nubMmUMLFiygU089lfbt20effvopPf300xSNRikUCtHcuXPpzDPPpFtvvZWi0SjV1tbSs88+K/5WmKtm4M4776T169fT5MmT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",
      "text/plain": [
       "<Figure size 640x480 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "net = ResNet(num_class).to(device)\n",
    "net.load_state_dict(torch.load(\"alex_cifar10_010.pth\"))\n",
    "net.eval()\n",
    "vis_data = tv.datasets.CIFAR10(\"./cifar10_dataset\", train=False, transform=transform)\n",
    "vis_iter = torch.utils.data.DataLoader(vis_data, batch_size=1, shuffle=True)\n",
    "\n",
    "_, ax = plt.subplots(1, 3)\n",
    "for i, (x, y) in enumerate(vis_iter):\n",
    "    if i >= 3:\n",
    "        break\n",
    "    x: Tensor = x.to(device)\n",
    "    y: Tensor = y.to(device)\n",
    "    pred: Tensor = net(x).argmax()\n",
    "    x.squeeze_()\n",
    "    y.squeeze_()\n",
    "    pred.squeeze_()\n",
    "    ax[i].set_title(f\"{y.data}, pred={pred.data}\")\n",
    "    ax[i].imshow(x.cpu().permute(1, 2, 0).numpy())\n",
    "    ax[i].axis('off')\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "dl",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
